import tkinter as tk
from tkinter import ttk, messagebox
from modbus.tcp_client import ModbusTCPClient
from modbus.serial_client import SerialClient
import serial
import serial.tools.list_ports
import threading
import time


class ModbusScannerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Modbus设备地址扫描工具")

        # 配置变量
        self.connection_type = tk.StringVar(value="TCP")
        self.start_address = tk.IntVar(value=1)
        self.end_address = tk.IntVar(value=255)
        self.function_code = tk.IntVar(value=3)
        self.start_register = tk.IntVar(value=0)
        self.register_count = tk.IntVar(value=1)  # 新增：读取寄存器数量
        self.tcp_host = tk.StringVar(value="127.0.0.1")
        self.tcp_port = tk.IntVar(value=10001)
        self.serial_port = tk.StringVar(value="COM1")
        self.baudrate = tk.IntVar(value=9600)
        self.parity = tk.StringVar(value="None") # None, Even, Odd
        self.stopbits = tk.IntVar(value=1) # 1, 1.5, 2
        self.bytesize = tk.IntVar(value=8) # 5, 6, 7, 8
        self.scan_interval = tk.IntVar(value=300)  # 搜索间隔时间，默认 300 毫秒
        self.timeout = tk.IntVar(value=500)  # 新增：超时时间，默认 500 毫秒

        # 停止扫描标志
        self.stop_scanning = False

        # 创建界面
        self.create_widgets()

    def create_widgets(self):
        # 连接方式选择
        frame_connection = ttk.LabelFrame(self.root, text="连接方式")
        frame_connection.grid(row=0, column=0, columnspan=2, padx=10, pady=5, sticky="ew")

        ttk.Radiobutton(frame_connection, text="TCP", variable=self.connection_type, value="TCP", command=self.update_connection_type).grid(row=0, column=0, padx=5, pady=5)
        ttk.Radiobutton(frame_connection, text="串口", variable=self.connection_type, value="Serial", command=self.update_connection_type).grid(row=0, column=1, padx=5, pady=5)

        # TCP 配置
        self.frame_tcp = ttk.LabelFrame(self.root, text="TCP 配置")
        self.frame_tcp.grid(row=1, column=0, columnspan=2, padx=10, pady=5, sticky="ew")

        ttk.Label(self.frame_tcp, text="IP地址:").grid(row=0, column=0, padx=5, pady=5, sticky="e")
        ttk.Entry(self.frame_tcp, textvariable=self.tcp_host).grid(row=0, column=1, padx=5, pady=5)

        ttk.Label(self.frame_tcp, text="端口:").grid(row=0, column=2, padx=5, pady=5, sticky="e")
        ttk.Entry(self.frame_tcp, textvariable=self.tcp_port).grid(row=0, column=3, padx=5, pady=5)

        # 串口配置
        self.frame_serial = ttk.LabelFrame(self.root, text="串口配置")
        self.frame_serial.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="ew")

        # ttk.Label(self.frame_serial, text="串口:").grid(row=0, column=0, padx=5, pady=5, sticky="e")
        # ttk.Entry(self.frame_serial, textvariable=self.serial_port).grid(row=0, column=1, padx=5, pady=5)

        # ttk.Label(self.frame_serial, text="波特率:").grid(row=0, column=2, padx=5, pady=5, sticky="e")
        # ttk.Entry(self.frame_serial, textvariable=self.baudrate).grid(row=0, column=3, padx=5, pady=5)
        # 动态获取系统可用串口列表
        available_ports = [port.device for port in serial.tools.list_ports.comports()]
        if not available_ports:
            available_ports = ["无可用串口"]

        ttk.Label(self.frame_serial, text="串口:").grid(row=0, column=0, padx=5, pady=5, sticky="e")
        ttk.Combobox(self.frame_serial, textvariable=self.serial_port, values=available_ports, state="readonly").grid(row=0, column=1, padx=5, pady=5)

        # 波特率选择
        baudrate_values = [600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]
        ttk.Label(self.frame_serial, text="波特率:").grid(row=0, column=2, padx=5, pady=5, sticky="e")
        ttk.Combobox(self.frame_serial, textvariable=self.baudrate, values=baudrate_values, state="readonly").grid(row=0, column=3, padx=5, pady=5)


        ttk.Label(self.frame_serial, text="校验位:").grid(row=1, column=0, padx=5, pady=5, sticky="e")
        ttk.Combobox(self.frame_serial, textvariable=self.parity, values=["None", "Even", "Odd"], state="readonly").grid(row=1, column=1, padx=5, pady=5)

        ttk.Label(self.frame_serial, text="数据位:").grid(row=1, column=2, padx=5, pady=5, sticky="e")
        ttk.Combobox(self.frame_serial, textvariable=self.bytesize, values=[5, 6, 7, 8], state="readonly").grid(row=1, column=3, padx=5, pady=5)

        ttk.Label(self.frame_serial, text="停止位:").grid(row=2, column=0, padx=5, pady=5, sticky="e")
        ttk.Combobox(self.frame_serial, textvariable=self.stopbits, values=[1, 1.5, 2], state="readonly").grid(row=2, column=1, padx=5, pady=5)

        # 扫描配置
        frame_scan = ttk.LabelFrame(self.root, text="扫描配置")
        frame_scan.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="ew")

        ttk.Label(frame_scan, text="起始地址:").grid(row=0, column=0, padx=5, pady=5, sticky="e")
        ttk.Entry(frame_scan, textvariable=self.start_address).grid(row=0, column=1, padx=5, pady=5)

        ttk.Label(frame_scan, text="结束地址:").grid(row=0, column=2, padx=5, pady=5, sticky="e")
        ttk.Entry(frame_scan, textvariable=self.end_address).grid(row=0, column=3, padx=5, pady=5)

        ttk.Label(frame_scan, text="功能码:").grid(row=1, column=0, padx=5, pady=5, sticky="e")
        ttk.Entry(frame_scan, textvariable=self.function_code).grid(row=1, column=1, padx=5, pady=5)

        ttk.Label(frame_scan, text="起始寄存器:").grid(row=1, column=2, padx=5, pady=5, sticky="e")
        ttk.Entry(frame_scan, textvariable=self.start_register).grid(row=1, column=3, padx=5, pady=5)

        ttk.Label(frame_scan, text="寄存器数量:").grid(row=2, column=0, padx=5, pady=5, sticky="e")
        ttk.Entry(frame_scan, textvariable=self.register_count).grid(row=2, column=1, padx=5, pady=5)

        ttk.Label(frame_scan, text="扫描间隔(毫秒):").grid(row=2, column=2, padx=5, pady=5, sticky="e")
        ttk.Entry(frame_scan, textvariable=self.scan_interval).grid(row=2, column=3, padx=5, pady=5)

        ttk.Label(frame_scan, text="超时时间(毫秒):").grid(row=3, column=0, padx=5, pady=5, sticky="e")
        ttk.Entry(frame_scan, textvariable=self.timeout).grid(row=3, column=1, padx=5, pady=5)

        # 扫描按钮
        self.scan_button = ttk.Button(self.root, text="开始扫描", command=self.start_scan)
        self.scan_button.grid(row=4, column=0, pady=10)

        # 停止按钮
        self.stop_button = ttk.Button(self.root, text="停止扫描", command=self.stop_scan)
        self.stop_button.grid(row=4, column=1, pady=10)

        # 扫描结果
        frame_results = ttk.LabelFrame(self.root, text="扫描结果")
        frame_results.grid(row=5, column=0, columnspan=2, padx=10, pady=5, sticky="nsew")

        self.result_listbox = tk.Listbox(frame_results, height=15)
        self.result_listbox.pack(fill="both", expand=True, padx=5, pady=5)

        # 初始化连接方式
        self.update_connection_type()

    def update_connection_type(self):
        """根据连接方式显示或隐藏配置部分"""
        if self.connection_type.get() == "TCP":
            self.frame_tcp.grid()
            self.frame_serial.grid_remove()
        else:
            self.frame_serial.grid()
            self.frame_tcp.grid_remove()

    def start_scan(self):
        # 禁用按钮防止重复点击
        self.scan_button.config(state="disabled")
        self.stop_scanning = False  # 重置停止标志
        self.result_listbox.delete(0, tk.END)

        # 获取用户输入
        connection_type = self.connection_type.get()
        start_address = self.start_address.get()
        end_address = self.end_address.get()
        function_code = self.function_code.get()
        start_register = self.start_register.get()
        register_count = self.register_count.get()  # 获取寄存器数量
        scan_interval = self.scan_interval.get() / 1000.0  # 转换为秒
        timeout = self.timeout.get() / 1000.0  # 转换为秒

        # 获取串口配置
        parity_map = {"None": serial.PARITY_NONE, "Even": serial.PARITY_EVEN, "Odd": serial.PARITY_ODD}
        stopbits_map = {1: serial.STOPBITS_ONE, 1.5: serial.STOPBITS_ONE_POINT_FIVE, 2: serial.STOPBITS_TWO}
        bytesize_map = {5: serial.FIVEBITS, 6: serial.SIXBITS, 7: serial.SEVENBITS, 8: serial.EIGHTBITS}

        parity = parity_map[self.parity.get()]
        stopbits = stopbits_map[self.stopbits.get()]
        bytesize = bytesize_map[self.bytesize.get()]

        # 启动扫描线程
        scan_thread = threading.Thread(
            target=self.scan_devices,
            args=(connection_type, start_address, end_address, function_code, start_register, register_count, scan_interval, timeout, parity, stopbits, bytesize),
        )
        scan_thread.start()

    def stop_scan(self):
        """停止扫描"""
        self.stop_scanning = True

    def scan_devices(self, connection_type, start_address, end_address, function_code, start_register, register_count, scan_interval, timeout, parity, stopbits, bytesize):
        try:
            if connection_type == "TCP":
                client = ModbusTCPClient(host=self.tcp_host.get(), port=self.tcp_port.get(), timeout=timeout)
                if not client.connect():
                    raise Exception("无法连接到 TCP 服务器")
            else:
                client = SerialClient(port=self.serial_port.get(), baudrate=self.baudrate.get(), timeout=timeout, parity=parity, stopbits=stopbits, bytesize=bytesize)
                if not client.connect():
                    raise Exception("无法打开串口")

            # 扫描地址范围
            for address in range(start_address, end_address + 1):
                if self.stop_scanning:  # 检查是否停止扫描
                    break

                result = client.read_register(start_register, function_code, register_count, unit=address)
                if result is not None:
                    self.result_listbox.insert(tk.END, f"地址 {address}: 收到数据 {result}")
                else:
                    self.result_listbox.insert(tk.END, f"地址 {address}: 无响应")
                time.sleep(scan_interval)  # 添加扫描间隔

            client.close()
        except Exception as e:
            messagebox.showerror("错误", str(e))
        finally:
            # 恢复按钮状态
            self.scan_button.config(state="normal")


if __name__ == "__main__":
    root = tk.Tk()
    app = ModbusScannerApp(root)
    root.mainloop()